feat(test): mountTestState/withTestState — one mounted state across several renderRoute calls - #525
Conversation
🦋 Changeset detectedLatest commit: bbf2019 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e4093a1f5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const opened = await openStateOwner(definition, definition.lifetime, renderer, options.driver); | ||
| let bindings: AgentMount.GeneratedRuntimeRequestBindings<TState, TEvents>; | ||
| try { | ||
| bindings = await opened.owner.requestBindings(options.signal === undefined ? {} : { signal: options.signal }); |
There was a problem hiding this comment.
Bind shared state operations to each render's abort signal
When a journey gives an individual renderRoute(..., { signal }) its own cancellation signal, this binding still uses only the mount-wide options.signal and is then reused by every render. createAgentStateHandle folds the binding signal into read/dispatch/changes (packages/rsc-runtime/src/state/handle.ts:31-34), so aborting one render will not stop its in-flight state operation; a slow dispatch can commit after cancellation and contaminate later steps in the shared-state test. Preserve the shared owner while binding operations to each render's request signal, or otherwise combine that signal with the mount-wide signal.
Useful? React with 👍 / 👎.
e0aa886 to
f5e5cd2
Compare
f5e5cd2 to
7021d22
Compare
…t for multi-render state journeys One mounted state owner (project state + notice ledger) shared across any number of renderRoute / renderRouteEvents calls, with typed read() and notices() snapshots and one close(). Same driver rules as the per-render mount. The worktree-proximity, host-test, and audiobook-curator examples drop their hand-rolled @agent-bundle/runtime/mount and /state mounts. Fixes #484
…t signal
A render that receives mountTestState()'s handles rebinds the shared owner
through requestBindings({ signal }) — the generated request scope's own
binding — so aborting one render stops its in-flight state operations
without disturbing the owner the other renders share. Request-lifetime
owners stay as the mount's one binding.
7021d22 to
bbf2019
Compare
Fixes #484.
What
agent-bundle/testnow exportsmountTestState()andwithTestState(): one state owner — the project's state definition plus its notice ledger — mounted once for a whole test, so a journey that records on one event and reads on the next runs against the samestate/noticeLedgerunder every render.mountTestState<TState, TEvents>(options?)→MountedTestStatewithcontext()({ state, noticeLedger }to spread into anyrenderRoute/renderRouteEventscontext),read()(typedAgentStateSnapshot<TState>),notices()(the ledger snapshot),close()(idempotent), plus the barestate/noticeLedgerhandles for a directdispatch.render.tsmountState), now shared through oneopenStateOwner:workspace-durable→ disposable sqlite root removed onclose(); other lifetimes → memory driver;options.driveroverrides and is closed with the mount.options.definitionmounts an explicit definition (checked typing) instead of the manifest's registeredsrc/state.ts;options.manifestpicks an explicit manifest. A manifest without state failsmanifest-unavailable; anexternaldefinition without a driver failsinvalid-input— both name the recovery.withTestState(run, options?)is the scoped form (try/finally owned by the helper).renderRoutealready honours caller-suppliedstate/noticeLedgerand mounts nothing when both are present.Shape decision: both helpers, since the scoped one is a five-line wrapper; the examples show each style (
beforeEach/afterEachwithmountTestState, single-testwithTestState).read()typing comes from type arguments or a checkedoptions.definition— the generatedRegistercarries route contracts only, no state type; registering the state type via typegen would be a separate change.Consumer code deleted
examples/worktree-proximity/tests/route-unit/routes.test.ts: themkdtemp/createSqliteStateDriver/createGeneratedRuntimeStatemount, the per-renderrequestBindings()/close()pair, and six morerequestBindings()pairs used only to read snapshots — replaced withmounted.context(),mounted.read(),mounted.notices(). No more@agent-bundle/runtime/mountor/state/sqliteimports.examples/host-test/tests/route-unit/routes.test.ts: same mount and per-render bindings removed (themkdtempthat remains is for the example's own log dir).examples/audiobook-curator/tests/route-unit/state.test.ts: the re-wrappeddefineState({ ...shelfStateDefinition, id, lifetime: 'process' })+createMemoryStateDriver+createAgentStateHandlemount replaced withwithTestState; it now renders against the project's real definition and gets a notice ledger too.Docs
website/docs/{en,zh}/guide/development/testing.mdx: new "One state across several renders" subsection and theroute-unitrow of the proof-level table.pnpm docs:site:buildpasses (parity, dead links).Tests
packages/agent-bundle/tests/route-unit/mount-test-state.test.ts(7 tests): state and ledger carried across renders and read back; isolated per-render mounts still work alongside; idempotent close; explicit definition typedread(); caller driver used and closed; scoped close on throw; no-state manifest and external-without-driver refusals.pnpm test:route-unit65/65;pnpm test:unitgreen exceptframework-plugin-registration.test.ts(pre-existing onmain, fixed by test(build): pass the project root to the composed configs in the plugin-registry test #523) and four load-induced timeouts that pass in isolation;pnpm typecheckclean apart from the same test(build): pass the project root to the composed configs in the plugin-registry test #523 file;pnpm lintclean.worktree-proximity(typecheck +test:routes10/10),host-test(typecheck +test7/7),audiobook-curator(typecheck +test:routes31/31).Changeset
.changeset/484-mount-test-state.md(agent-bundle: patch).Review status
2e4093a1f— Codex reviewed; one P1 thread onrender.ts(mountTestStatehandles were bound only to the mount-wide signal, so a render's ownsignaldid not cancel its in-flight state operations). Addressed inf5e5cd223: a render that receives the mounted handles rebinds the shared owner throughrequestBindings({ signal })— the generated request scope's own binding — so itssignalcancels its state operations while the owner the other renders share is untouched;request-lifetime owners keep the mount's single binding. Covered by the new "binds each render to its own signal without disturbing the shared owner" test. Docs (en + zh) state the per-render binding.mainat test(build): pass the project root to the composed configs in the plugin-registry test #523 (picks up theframework-plugin-registrationtypecheck fix). Awaiting the reviewer on the current head; per this task's rules no PR comments are posted, so the re-review is not requested via@codex review.